# message.rb - Message handler class
# Copyright (C) 2004-2009 Akira TAGOH

# Authors:
#   Akira TAGOH  <akira@tagoh.org>

# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA.

require 'singleton'
require 'prune/debug'
require 'prune/pattern'
require 'prune/types'
require 'prune/error'


module PRUNE

=begin rdoc

== PRUNE::MessageHandler

=end

  class MessageHandler
    include Singleton
    include PRUNE::Debug

=begin rdoc

=== PRUNE::MessageHandler#new(msg)

=end

    def new(msg)
      command = PRUNE::Message.normalize(msg[:command])
      PRUNE.Fail(PRUNE::Error::InvalidMessage, debug_sprintf("%s", msg)) if command.nil?
      if msg[:command] !~ /\A\d+/ then
        PRUNE.Fail(PRUNE::Error::InvalidMessage, debug_sprintf("%s", msg)) unless PRUNE::Message.valid_command?(command)
      end
      eval("PRUNE::Message::#{command}.new(msg)")
    end # def new

  end # class MessageHandler

=begin rdoc

== PRUNE::Message

=end

  module Message

    CH_PARAM = 1
    CH_TRAILING = 2

    @@ChannelLocationMap = {}
    @@ResponseMap = {}
    @@ReverseResponseMap = {}
    @@CommandResponseMap = {}
    @@ParametersMap = {}

    class << self

=begin rdoc

=== PRUNE::Message.check_params

=end

      def check_params
        @check_params = false unless defined?(@check_params)
        @check_params
      end # def check_params

=begin rdoc

=== PRUNE::Message.params_check_enabled?

=end

      def params_check_enabled?
        @check_params != false
      end # def params_check_enabled?

=begin rdoc

=== PRUNE::Message.check_params=(value)

=end

      def check_params=(value)
        @check_params = false unless defined?(@check_params)

        if value != :strict && value != true then
          @check_params = false
        else
          @check_params = value
        end
      end # def check_params

=begin rdoc

=== PRUNE::Message.normalize(cmd)

=end

      def normalize(cmd)
        if cmd.kind_of?(Symbol) then
          cmd = cmd.to_s.upcase
        end
        if (cmd =~ /\A\d+\Z/ || cmd =~ /\A-\d+\Z/) && @@ReverseResponseMap.include?(cmd) then
          @@ReverseResponseMap[cmd].to_s.upcase
        elsif cmd.kind_of?(String) then
          cmd.upcase
        else
          cmd
        end
      end # def normalize

=begin rdoc

=== PRUNE::Message.normalize2(cmd)

=end

      def normalize2(cmd)
        if cmd.kind_of?(Symbol) then
          cmd = cmd.to_s.upcase
        end
        if (cmd =~ /\A\d+\Z/ || cmd =~ /\A-\d+\Z/) && @@ReverseResponseMap.include?(cmd) then
          cmd.to_s
        elsif cmd.kind_of?(String) then
          cmd.upcase
        else
          cmd
        end
      end # def normalize2

=begin rdoc

=== PRUNE::Message.sanitize(cmd)

=end

      def sanitize(cmd)
        result = normalize(cmd)
        result.to_sym unless result.nil?
      end # def sanitize

=begin rdoc

=== PRUNE::Message.sanitize2(cmd)

=end

      def sanitize2(cmd)
        result = normalize2(cmd)
        result.to_sym unless result.nil?
      end # def sanitize2

=begin rdoc

=== PRUNE::Message.channelinfo(cmd)

=end

      def channelinfo(cmd)
        @@ChannelLocationMap[sanitize(cmd)]
      end # def get_chinfo

=begin rdoc

=== PRUNE::Message.valid_command?(cmd)

=end

      def valid_command?(cmd)
        @@CommandResponseMap.keys.include?(sanitize(cmd)) || @@ResponseMap.include?(sanitize(cmd))
      end # def valid_command?

=begin rdoc

=== PRUNE::Message.min_params(cmd)

=end

      def min_params(cmd)
        v = @@ParametersMap[sanitize(cmd)]
        if v.class == Range then
          v.to_a[0]
        elsif v.class == Array then
          v[0]
        else
          v
        end
      end # def min_params

=begin rdoc

=== PRUNE::Message.max_params(cmd)

=end

      def max_params(cmd)
        v = @@ParametersMap[sanitize(cmd)]
        if v.class == Range then
          v.to_a[-1]
        elsif v.class == Array then
          v[-1]
        else
          v
        end
      end # def max_params(cmd)

=begin rdoc

=== PRUNE::Message.n_params(cmd)

=end

      def n_params(cmd)
        @@ParametersMap[sanitize(cmd)]
      end # def n_params

    end

    PRUNE::PATTERN::Responses.each do |line|
      num, symbol = line.split(',', 2)
      @@ResponseMap[symbol] = num
      @@ResponseMap[symbol.to_sym] = num
      @@ReverseResponseMap[num] = symbol.to_sym
    end
    %w(JOIN,KICK,MODE,NAMES,NOTICE,PART,PRIVMSG,TOPIC,WHO|CH_PARAM,0
       INVITE,315,322,324,329,332,333,352,366,367,368,403,404,405,437,442,467,471,473,474,475,476,477,478,482|CH_PARAM,1
       341,353,441,443|CH_PARAM,2
    ).each do |line|
      commands, info = line.split('|')
      loc, idx = info.split(',')
      commands.split(',').each do |cmd|
        @@ChannelLocationMap[PRUNE::Message.sanitize(cmd)] = PRUNE::TYPE::ChannelLocationInfo.new(eval(loc), idx.to_i)
      end
    end
    PRUNE::PATTERN::ResponsesMap.each do |line|
      cmd, *resp = line.split(',')
      @@CommandResponseMap[cmd.to_sym] = resp
    end
    PRUNE::PATTERN::ParametersMap.each do |line|
      cmd, nparams = line.split('|')
      @@ParametersMap[PRUNE::Message.sanitize(cmd)] = eval(nparams)
    end

=begin rdoc

=== PRUNE::Message::Core

=end

    class Core
      include PRUNE::Debug

=begin rdoc

==== PRUNE::Message::Core#new(msg)

=end

      def initialize(msg)
        if !msg.kind_of?(Hash) ||
            !msg.include?(:prefix) || !msg.include?(:command) ||
            !msg.include?(:params) || !msg[:params].kind_of?(Array) ||
            msg[:params].map! {|x| x.kind_of?(Numeric) ? x.to_s : x}.map {|x| x.kind_of?(String) || x.nil?}.include?(false) then
          PRUNE.Fail(PRUNE::Error::InvalidMessage, msg.inspect)
        end
        @message = msg
        @message[:time] = Time.now unless @message.has_key?(:time)
        @enforced_delivery = false
        @enforced_charset = nil
      end # def initialize

      attr_reader :enforced_charset

=begin rdoc

==== PRUNE::Message::Core#enforced_delivery=(flag)

=end

      def enforced_delivery=(flag)
        @enforced_delivery = (flag == true)
      end # def enforced_delivery=

=begin rdoc

==== PRUNE::Message::Core#is_enforced_delivery?

=end

      def is_enforced_delivery?
        @enforced_delivery == true
      end # def is_enforced_delivery?

=begin rdoc

==== PRUNE::Message::Core#enforced_charset=(value)

=end

      def enforced_charset=(value)
        begin
          Iconv.conv(value, value, "foo")
          @enforced_charset = value
        rescue Errno::EINVAL
          PRUNE.Fail(PRUNE::Error::InvalidCharset, value)
        end
      end # def enforced_charset=

=begin rdoc

==== PRUNE::Message::Core#nick

=end

      def nick
        @message[:prefix][:nick]
      end # def nick

=begin rdoc

==== PRUNE::Message::Core#nick=(new)

=end

      def nick=(new)
        @message[:prefix][:nick] = new
      end # def nick=

=begin rdoc

==== PRUNE::Message::Core#user(noident = false)

=end

      def user(noident = false)
        if noident then
          @message[:prefix][:user].sub(/\A~/, "") unless @message[:prefix][:user].nil?
        else
          @message[:prefix][:user]
        end
      end # def user

=begin rdoc

==== PRUNE::Message::Core#user=(new)

=end

      def user=(new)
        @message[:prefix][:user] = new
      end # def user=

=begin rdoc

==== PRUNE::Message::Core#host

=end

      def host
        @message[:prefix][:host]
      end # def host

=begin rdoc

==== PRUNE::Message::Core#host=(new)

=end

      def host=(new)
        @message[:prefix][:host] = new
      end # def host=

=begin rdoc

==== PRUNE::Message::Core#time

=end

      def time
        @message[:time]
      end

=begin rdoc

==== PRUNE::Message::Core#command

=end

      def command
        @message[:command]
      end # def command

=begin rdoc

==== PRUNE::Message::Core#params(num = nil)

=end

      def params(num = nil)
        if num.nil? then
          @message[:params]
        else
          raise TypeError, sprintf("Can't convert %s into Fixnum", num.class) unless num.kind_of?(Integer)
          @message[:params][num]
        end
      end # def params

=begin rdoc

==== PRUNE::Message::Core#channel(cmd, opt = {:suffix=>false, :array=>false})

=end

      def channel(cmd = nil, opt = {:suffix=>false, :array=>false})
        raise RuntimeError, "No real method of channel" if cmd.nil?

        retval = []
        map = PRUNE::Message.channelinfo(cmd)
        unless map.nil? then
          case map.location
          when CH_PARAM
            unless self.params(map.index).nil? then
              retval = self.params(map.index).split(',')
            end
          when CH_TRAILING
            unless self.params(-1).nil? then
              retval = self.params(-1).split(',')
            end
          end
          if opt[:suffix] == true then
            (0..retval.length-1).each do |i|
              retval[i] = sprintf("%s%s", retval[i], @message[:suffix])
            end
          end
        end # unless map.nil?

        retval = retval[0] if !opt.has_key?(:array) || opt[:array] == false
        return retval
      end # def channel

=begin rdoc

==== PRUNE::Message::Core#to_s(cmd)

=end

      def to_s(cmd = nil)
        raise RuntimeError, "No real method of to_s" if cmd.nil?

        retval = ""
        map = PRUNE::Message.channelinfo(cmd)
        msg = Marshal.load(Marshal.dump(@message))
        msg[:suffix] = "" if msg[:suffix].nil?
        unless map.nil? then
          index = -1
          case map.location
          when CH_PARAM
            index = map.index
          when CH_TRAILING
            index = -1
          end
          unless msg[:params][index].nil? then
            msg[:params][index] = msg[:params][index].split(',').map {|x| sprintf("%s%s", x, msg[:suffix])}.join(',')
          end
        end
        retval << ":" if (!self.nick.nil? && !self.user.nil? && !self.host.nil?) || (self.nick.nil? && self.user.nil? && !self.host.nil?)
        if !msg[:suffix].nil? && !msg[:suffix].empty? &&
            !self.channel.nil? && self.channel !~ /#{PRUNE::PATTERN::CHANNEL}/ then
          retval << sprintf("%s%s", self.nick, msg[:suffix]) if !self.nick.nil? && !self.user.nil? && !self.host.nil?
        else
          retval << sprintf("%s", self.nick) if !self.nick.nil? && !self.user.nil? && !self.host.nil?
        end
        retval << sprintf("!%s@", self.user) if !self.nick.nil? && !self.user.nil? && !self.host.nil?
        retval << sprintf("%s", self.host) if  !self.nick.nil? && !self.user.nil? && !self.host.nil? || self.nick.nil? && self.user.nil? && !self.host.nil?
        retval << " " if retval.length > 1
        retval << sprintf("%s", PRUNE::Message.normalize2(cmd))
        retval << " " if !self.params.empty?
        msg[:params][0..-2].each do |p|
          retval << sprintf("%s ", p)
        end
        retval << sprintf(":%s", msg[:params][-1]) if !msg[:params][-1].nil? && !msg[:params][-1].empty?
#        retval.sub!(/ \Z/, '')
        retval << "\r\n"
      end # def to_s

=begin rdoc

==== PRUNE::Message::Core#suffix

=end

      def suffix
        @message[:suffix]
      end # def suffix

=begin rdoc

==== PRUNE::Message::Core#suffix=(new)

=end

      def suffix=(new)
        @message[:suffix] = new
      end # def suffix=

=begin rdoc

==== PRUNE::Message::Core#==(obj)

=end

      def ==(obj)
        return false unless obj.kind_of?(PRUNE::Message::Core)

        self.to_s == obj.to_s
      end # def ==

    end # class Core

    PRUNE::PATTERN::Commands.map {|n| n.split('|')}.flatten.each do |cmd|
      begin
        module_eval <<-EOS, __FILE__, __LINE__+1
        class #{PRUNE::Message.normalize(cmd)} < PRUNE::Message::Core
          def initialize(arg, *args)
            msg = nil
            unless arg.kind_of?(Hash) then
              msg = {:prefix=>{:nick=>nil,
                               :user=>nil,
                               :host=>nil},
                     :command=>"#{PRUNE::Message.normalize(cmd)}",
                     :params=>args.unshift(arg),
                     :time=>Time.now,
              }
            else
              msg = arg
              msg[:command] = '#{PRUNE::Message.normalize(cmd)}'
            end
            PRUNE.Fail(PRUNE::Error::LessParamsInMessage, msg[:params].length, PRUNE::Message.min_params(msg[:command]), debug_sprintf("%s", msg)) if PRUNE::Message.params_check_enabled? && msg[:params].length < PRUNE::Message.min_params(msg[:command])
            PRUNE.Fail(PRUNE::Error::MoreParamsInMessage, msg[:params].length, PRUNE::Message.max_params(msg[:command]), debug_sprintf("%s", msg)) if PRUNE::Message.check_params == :strict && msg[:params].length > PRUNE::Message.max_params(msg[:command])
            super(msg)
          end
          def channel(opt = {:suffix=>false, :array=>false})
            super("#{PRUNE::Message.normalize(cmd)}", opt)
          end # def channel
          def to_s
            super("#{PRUNE::Message.normalize2(cmd)}")
          end # def to_s
          alias :inspect :to_s
        end
        EOS
      rescue SyntaxError
        raise NameError, sprintf("invalid identifier %s%s", cmd, caller(3))
      end
    end
    @@ResponseMap.each do |key,val|
      if key.kind_of?(Symbol) then
        begin
          module_eval <<-EOS, __FILE__, __LINE__+1
          class #{key} < PRUNE::Message::Core
            def initialize(arg, *args)
              msg = nil
              unless arg.kind_of?(Hash) then
                msg = {:prefix=>{:nick=>nil,
                                 :user=>nil,
                                 :host=>nil},
                       :command=>"#{val}",
                       :params=>args.unshift(arg),
                       :time=>Time.now,
                }
              else
                msg = arg
                msg[:command] = '#{val}'
              end
              super(msg)
            end
            def channel(opt = {:suffix=>false, :array=>false})
              super("#{PRUNE::Message.normalize(val)}", opt)
            end # def channel
            def to_s
              super("#{PRUNE::Message.normalize2(val)}")
            end # def to_s
            alias :inspect :to_s
            def to_sym
              #{key}
            end # def to_sym
            def to_num
              #{val}
            end
          end
          EOS
        rescue SyntaxError
          raise NameError, sprintf("invalid identifier %s\n%s", val, caller(3))
        end
      end
    end

=begin rdoc

=== PRUNE::Message::RAW

=end

    class RAW < PRUNE::Message::Core

      def initialize(arg)
        msg = nil
        @string = nil
        if arg.kind_of?(Hash) then
          msg = arg
        elsif arg.kind_of?(String) then
          @string = arg
          msg = {:prefix=>{:nick=>nil,
                           :user=>nil,
                           :host=>nil},
                 :command=>nil,
                 :params=>[],
                 :time=>Time.now,
          }
        else
          raise TypeError, sprintf("Can't convert %s into Hash", arg.class)
        end
        super(msg)
      end

      def channel(opt = {:suffix=>false, :array=>false})
        if @message.has_key?(:command) && !@message[:command].nil? then
          super(PRUNE::Message.normalize(@message[:command]), opt)
        else
          nil
        end
      end # def channel

      def to_s
        if @message.has_key?(:command) && !@message[:command].nil? then
          super(PRUNE::Message.normalize(@message[:command]))
        elsif !@string.nil? then
          @string
        else
          ""
        end
      end # def to_s

      alias :inspect :to_s

    end # class RAW

  end # module Message

end # module PRUNE
